package com.kunpan.admin.async;

import com.kunpan.admin.common.Enum.BusinessType;
import com.kunpan.admin.common.Enum.FileStatusEnum;
import com.kunpan.admin.common.redis.RedisCache;
import com.kunpan.admin.common.redis.RedisMessagePublisher;
import com.kunpan.admin.common.utils.file.FileUtils;
import com.kunpan.admin.common.utils.file.MimeTypeUtils;
import com.kunpan.admin.constant.Constants;
import com.kunpan.admin.entity.ChunkFile;
import com.kunpan.admin.entity.PanFile;
import com.kunpan.admin.entity.User;
import com.kunpan.admin.entity.dto.MessageDto;
import com.kunpan.admin.mapper.PanFileMapper;
import com.kunpan.admin.mapper.UserMapper;
import com.kunpan.admin.properties.MinioProperties;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

@Service
public class MinioFileUploadAsyncService {

    private static final Logger logger = LoggerFactory.getLogger(MinioFileUploadAsyncService.class);

    @Autowired
    private MinioProperties minioProperties;

    @Autowired
    private MinioClient minioClient;

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private PanFileMapper panFileMapper;

    @Autowired
    private RedisMessagePublisher redisMessagePublisher;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    @Async("taskExecutor") // 定义一个异步方法，设置线程池配置类的bean名称为taskExecutor
    public void minioChunkUpload(InputStream fileInputStream, ChunkFile chunkFile, String userId) throws IOException {
        String fileMd5 = chunkFile.getFileMd5();
        Integer chunkIndex = chunkFile.getChunkNumber();
        logger.info("minioChunkUpload fileName:{},fileMd5:{}", chunkFile.getFileName(), chunkFile.getFileMd5());
        //先判断当前文件分片是否上传完毕
        if (redisCache.getStatus(fileMd5, chunkIndex)) {
            if (redisCache.getAllChunkStatus(fileMd5).size() == chunkFile.getTotalChunks()) {
                insetFileInfo(chunkFile, userId);
            }
            //提前结束方法
            return;
        }
        // 自定义临时文件目录
        // 生成分片的唯一标识
        StringBuilder chunkSb = new StringBuilder();
        String chunkMark = chunkSb.append(fileMd5).append("/").append("chunk_").append(chunkIndex).toString();
        chunkSb.setLength(0);
        String cachePathKey = String.format(Constants.FILE_PATH_PREFIX, fileMd5);
        String path;
        if (redisCache.hasKey(cachePathKey)) {//判断文件缓存中是否有当前分片文件的路径值
            path = redisCache.getCacheObject(cachePathKey);
        } else {
            //根据文件类型生成对应的文件路径
            //获取文件类型字符串
            String fileType = MimeTypeUtils.FILE_TYPE[FileUtils.getMultipartFileType(chunkFile.getFileName())];
            path = chunkSb.append(minioProperties.getUploadPath()).append(fileType).append("/").toString();
            redisCache.setCacheObject(cachePathKey, path);
            chunkSb.setLength(0);
        }
        //上传分片文件
        try {
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(minioProperties.getBucketName())
                            .object(chunkSb.append(path).append(chunkMark).toString())
                            .stream(fileInputStream, chunkFile.getCurrentChunkSize(), -1)
                            .contentType("multipart/form-data")
                            .build());
            redisCache.setStatus(fileMd5, chunkIndex, true);
            //判断所有文件是否上传完毕
            if (redisCache.getAllChunkStatus(fileMd5).size() == chunkFile.getTotalChunks()) {
                //合并文件
                insetFileInfo(chunkFile, userId);
            }
        } catch (Exception e) {
            logger.error("文件分片上传出现错误 fileMd5:{}", fileMd5);
            //出现异常时
            throw new RuntimeException(e);
        } finally {
            if (fileInputStream != null) {
                fileInputStream.close();
            }
        }

    }

    /**
     * 批量删除
     */
    @Async("taskExecutor")
    public void minioChunkDelete(List<DeleteObject> dos) {
        logger.info("开始批量删除文件");
        try {
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder()
                    .bucket(minioProperties.getBucketName())
                    .objects(dos)
                    .build());
            for (Result<DeleteError> result : results) {
                DeleteError error = result.get();
                logger.error("Error in deleting object " + error.objectName() + "; " + error.message());
            }
        } catch (Exception e) {
            logger.error("文件批量删除出现错误");
            throw new RuntimeException(e);
        }
    }

    /**
     * 根据文件路径前缀删除取消上传的分片文件
     */
    @Async("taskExecutor")
    public void deleteChuckFile(String fileMd5) {
        String cachePathKey = String.format(Constants.FILE_PATH_PREFIX, fileMd5);
        String path = redisCache.getCacheObject(cachePathKey);
        String prefix = path + fileMd5 + "/";
        Iterable<Result<Item>> results1 = minioClient.listObjects(
                ListObjectsArgs.builder()
                        .bucket(minioProperties.getBucketName())
                        .prefix(prefix)
                        .recursive(true).build());
        List<DeleteObject> list = new ArrayList<>();
        for (Result<Item> result : results1) {
            Item item = null;
            try {
                item = result.get();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            list.add(new DeleteObject(item.objectName()));
            minioChunkDelete(list);
            //删除当前文件在上传过程中存储的各种数据
            redisCache.deleteKeysByPattern(String.format(Constants.FILE_DELETE_PREFIX, fileMd5));
            redisCache.deleteStatus(fileMd5);
        }
    }

    public String mergeChunks(String fileMd5, Integer shardTotal, String fileName) {
        logger.info("开始合并文件,md5:{}", fileMd5);
        // 生成合并后的文件唯一标识
        String cacheKey = String.format(Constants.FILE_PATH_PREFIX, fileMd5);
        String path = "";
        if (redisCache.hasKey(cacheKey)) {
            path = redisCache.getCacheObject(cacheKey);
        }
        //String mergedFileKey = fileMd5 + "_merged_" + userId;
        StringBuilder mergeSb = new StringBuilder();
        //分片集合
        List<ComposeSource> sources = new ArrayList<>();
        String dosKey = String.format(Constants.FILE_DELETE_SOURCES_PREFIX, fileMd5);
        List<DeleteObject> dos = redisCache.getCacheList(dosKey);
        // 合并分片文件
        for (int i = 1; i <= shardTotal; i++) {
            String chunkKeyPath = mergeSb.append(path).append(fileMd5).append("/").append("chunk_").append(i).toString();
            //将当前分片文件添加到 sources 集合中
            sources.add(ComposeSource.builder().bucket(minioProperties.getBucketName()).object(chunkKeyPath).build());
            //添加到批量删除的文件路径集合中
            dos.add(new DeleteObject(chunkKeyPath));
            //重置当前mergeSb中的字符串
            mergeSb.setLength(0);
        }
        // 创建 ComposeObjectArgs 对象，并设置目标 bucket 和 object 名称，以及分片文件列表
        String mergedFilePath = mergeSb.append(path).append(fileName).toString();
        try {
            minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(minioProperties.getBucketName())
                            .object(mergedFilePath)
                            .sources(sources)
                            .build());
        } catch (Exception e) {
            logger.error("文件合并出现错误");
            throw new RuntimeException(e);
        }
        // 使用异步方式删除分片文件
        minioChunkDelete(dos);
        //删除文件的同时删除当前文件存在缓存中的路径数据
//        redisCache.deleteObject(cacheKey);
        //合并完成后删除文件状态缓存
        //使用通配符删除所有匹配改键的数据
        redisCache.deleteKeysByPattern(String.format(Constants.FILE_DELETE_PREFIX, fileMd5));
        redisCache.deleteStatus(fileMd5);
        mergeSb.setLength(0);
        return mergeSb.append(minioProperties.getUrl()).append("/")
                .append(minioProperties.getBucketName()).append("/")
                .append(mergedFilePath).toString();
    }

    public void insetFileInfo(ChunkFile chunkFile, String userId) {
        PanFile panFile = new PanFile();
        //合并文件
        String filePath = mergeChunks(chunkFile.getFileMd5(), chunkFile.getTotalChunks(), chunkFile.getFileName());
        // 设置合并后的文件信息
        panFile.setFileStatus(FileStatusEnum.NORMAL.getValue());
        Date now = new Date();
        panFile.setCreateTime(now);
        panFile.setUpdateTime(now);
        panFile.setUserId(Integer.valueOf(userId));
        panFile.setFileType(FileUtils.getMultipartFileType(chunkFile.getFileName()));
        panFile.setFileMd5(chunkFile.getFileMd5());
        panFile.setParentId(chunkFile.getParentId());
        panFile.setFileSize(chunkFile.getTotalSize());
        panFile.setFileName(chunkFile.getFileName());
        panFile.setFilePath(filePath);
        transactionTemplate.execute((TransactionCallback<Void>) status -> {
            try {
                // 插入数据库记录
                if (panFileMapper.insertFile(panFile) > 0) {
                    // 更新用户空间
//                    User user = userMapper.selectUserById(Integer.valueOf(userId));
                    userMapper.updateUserSpace(Integer.valueOf(userId), chunkFile.getTotalSize());
                    //调用静态工厂方法创建对象
                    MessageDto messageDto = MessageDto.create(Integer.valueOf(userId),
                            panFile.getFileType(), chunkFile.getTotalSize(),BusinessType.UPLOAD.toString());
                    //发布清除对应缓存的消息
                    redisMessagePublisher.publishFileUploadEvent(messageDto);
                    //通知前端文件分片合并成功
                    Map<String, String> messaageMap = new HashMap<>();
                    messaageMap.put("identifier", chunkFile.getIdentifier());
                    messaageMap.put("status", "merged");
                    simpMessagingTemplate.convertAndSendToUser(userId, "/progress", messaageMap);
                }
            } catch (Exception e) {
                // 手动设置事务回滚
                status.setRollbackOnly();
                throw new RuntimeException(e);
            }
            return null;
        });
    }
}